/*
 * Copyright (C) 2021 Huawei Device Co., Ltd.
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */

package com.flaviofaria.kenburnsview;

import ohos.agp.components.AttrSet;
import ohos.agp.components.Component;
import ohos.agp.components.Image;
import ohos.agp.components.element.Element;
import ohos.agp.render.Canvas;
import ohos.agp.render.Paint;
import ohos.agp.render.PixelMapHolder;
import ohos.agp.utils.Matrix;
import ohos.agp.utils.RectFloat;
import ohos.app.Context;
import ohos.app.dispatcher.TaskDispatcher;
import ohos.media.image.PixelMap;

/**
 * KenBurnsView
 *
 * @since 2021.07.12
 */
public class KenBurnsView extends Image implements Component.DrawTask {
    /**
     * A {@link TransitionListener} to be notified when
     * a transition starts or ends.
     */
    private TransitionListener mTransitionListener;

    /**
     * The ongoing transition.
     */
    private Transition mCurrentTrans;
    /**
     * The progress of the animation, in milliseconds.
     */
    private long mElapsedTime;
    /**
     * The time, in milliseconds, of the last animation frame.
     * This is useful to increment {@link #mElapsedTime} regardless
     * of the amount of time the animation has been paused.
     */
    private long mLastFrameTime;
    /**
     * Controls whether the the animation is running.
     */
    private boolean mPaused;
    /**
     * Indicates whether the parent constructor was already called.
     * This is needed to distinguish if the image is being set before
     * or after the super class constructor returns.
     */
    private boolean mInitialized;

    private RectFloat mDrawableRect;
    private TaskDispatcher mGlobalTaskDispatcher;
    private PixelMap mPixelMap;

    /**
     * Matrix used to perform all the necessary transition transformations.
     */
    private final Matrix mMatrix = new Matrix();
    /**
     * The {@link TransitionGenerator} implementation used to perform the transitions between
     * rects. The default {@link TransitionGenerator} is {@link RandomTransitionGenerator}.
     */
    private TransitionGenerator mTransGen = new RandomTransitionGenerator();
    /**
     * The rect that holds the bounds of this view.
     */
    private final RectFloat mViewportRect = new RectFloat();

    /**
     * KenBurnsView instance
     *
     * @param context 上下文
     */
    public KenBurnsView(Context context) {
        this(context, null);
    }

    /**
     * KenBurnsView instance
     *
     * @param context 上下文
     * @param attrSet AttrSet
     */
    public KenBurnsView(Context context, AttrSet attrSet) {
        this(context, attrSet, "");
    }

    /**
     * KenBurnsView instance
     *
     * @param context 上下文
     * @param attrSet AttrSet
     * @param styleName styleName
     */
    public KenBurnsView(Context context, AttrSet attrSet, String styleName) {
        super(context, attrSet, styleName);
        mInitialized = true;
        /**
         * Attention to the super call here!
         */
        super.setScaleMode(ScaleMode.CLIP_CENTER);
        mGlobalTaskDispatcher = context.getUITaskDispatcher();
        addDrawTask(this);
        setBindStateChangedListener(new BindStateChangedListener() {
            @Override
            public void onComponentBoundToWindow(Component component) {
                restart();
            }

            @Override
            public void onComponentUnboundFromWindow(Component component) {
            }
        });
    }

    @Override
    public void setVisibility(int visibility) {
        super.setVisibility(visibility);
        /**
         * When not visible, onDraw() doesn't get called,
         * but the time elapses anyway.
         */
        switch (visibility) {
            case VISIBLE:
                resume();
                break;
            default:
                pause();
                break;
        }
    }

    @Override
    public void setBackground(Element element) {
        super.setBackground(element);
        handleImageChange();
    }

    /**
     * Generates and starts a transition.
     */
    private void startNewTransition() {
        if (!hasBounds()) {
            /**
             * Can't start transition if the drawable has no bounds.
             */
            return;
        }
        mCurrentTrans = mTransGen.generateNextTransition(mDrawableRect, mViewportRect);
        mElapsedTime = 0;
        mLastFrameTime = System.currentTimeMillis();
        fireTransitionStart(mCurrentTrans);
    }

    /**
     * Creates a new transition and starts over.
     */
    public void restart() {
        int width = getWidth();
        int height = getHeight();

        if (width == 0 || height == 0) {
            /**
             * Can't call restart() when view area is zero.
             */
            return;
        }

        updateViewport(width, height);
        updateDrawableBounds();

        startNewTransition();
    }

    /**
     * Checks whether this view has bounds.
     *
     * @return true:hasBounds;false:noBounds
     */
    private boolean hasBounds() {
        return !mViewportRect.isEmpty();
    }

    /**
     * Fires a start event on {@link #mTransitionListener};
     *
     * @param transition the transition that just started.
     */
    private void fireTransitionStart(Transition transition) {
        if (mTransitionListener != null && transition != null) {
            mTransitionListener.onTransitionStart(transition);
        }
    }

    /**
     * Fires an end event on {@link #mTransitionListener};
     *
     * @param transition the transition that just ended.
     */
    private void fireTransitionEnd(Transition transition) {
        if (mTransitionListener != null && transition != null) {
            mTransitionListener.onTransitionEnd(transition);
        }
    }

    /**
     * Updates the viewport rect. This must be called every time the size of this view changes.
     *
     * @param width the new viewport with.
     * @param height the new viewport height.
     */
    private void updateViewport(float width, float height) {
        mViewportRect.left = 0;
        mViewportRect.top = 0;
        mViewportRect.right = width;
        mViewportRect.bottom = height;
    }

    /**
     * Updates the drawable bounds rect. This must be called every time the drawable
     * associated to this view changes.
     */
    private void updateDrawableBounds() {
        if (mDrawableRect == null) {
            mDrawableRect = new RectFloat();
        }

        mDrawableRect.left = 0;
        mDrawableRect.top = 0;
        mDrawableRect.right = 1974;
        mDrawableRect.bottom = 2961;
    }

    /**
     * This method is called every time the underlying image
     * is changed.
     */
    private void handleImageChange() {
        updateDrawableBounds();
        /**
         * Don't start a new transition if this event
         * was fired during the super constructor execution.
         * The view won't be ready at this time. Also,
         * don't start it if this view size is still unknown.
         */
        if (mInitialized) {
            startNewTransition();
        }
    }

    /**
     * Pauses the Ken Burns Effect animation.
     */
    public void pause() {
        mPaused = true;
    }

    /**
     * Resumes the Ken Burns Effect animation.
     */
    public void resume() {
        mPaused = false;
        // This will make the animation to continue from where it stopped.
        mLastFrameTime = System.currentTimeMillis();
        invalidate();
    }

    /**
     * setPixelMap
     *
     * @param pixelMap 对象
     */
    public void setPixelMap(PixelMap pixelMap) {
        this.mPixelMap = pixelMap;
    }

    @Override
    public void onDraw(Component component, Canvas canvas) {
        if (!mPaused && mPixelMap != null) {
            mPixelMap.getImageInfo().size.width = 1974;
            mPixelMap.getImageInfo().size.height = 2961;
            if (mDrawableRect.isEmpty()) {
                updateDrawableBounds();
            } else if (hasBounds()) {
                if (mCurrentTrans == null) {
                    /**
                     * Starting the first transition.
                     */
                    startNewTransition();
                }

                /**
                 * If null, it's supposed to stop.
                 */
                if (mCurrentTrans.getDestinyRect() != null) {
                    handleCanvas(canvas);
                } else {
                    /**
                     * Stopping? A stop event has to be fired.
                     */
                    fireTransitionEnd(mCurrentTrans);
                }
            } else {
                mLastFrameTime = System.currentTimeMillis();
            }
            mLastFrameTime = System.currentTimeMillis();

            mGlobalTaskDispatcher.asyncDispatch(new Runnable() {
                @Override
                public void run() {
                    invalidate();
                }
            });
        }
    }

    private void handleCanvas(Canvas canvas) {
        mElapsedTime += System.currentTimeMillis() - mLastFrameTime;
        RectFloat currentRect = mCurrentTrans.getInterpolatedRect(mElapsedTime);

        float widthScale = mDrawableRect.getWidth() / currentRect.getWidth();
        float heightScale = mDrawableRect.getHeight() / currentRect.getHeight();
        /**
         * Scale to make the current rect match the smallest drawable dimension.
         */
        float currRectToDrwScale = Math.min(widthScale, heightScale);
        /**
         * Scale to make the current rect match the viewport bounds.
         */
        float vpWidthScale = mViewportRect.getWidth() / currentRect.getWidth();
        float vpHeightScale = mViewportRect.getHeight() / currentRect.getHeight();
        float currRectToVpScale = Math.min(vpWidthScale, vpHeightScale);
        /**
         * Combines the two scales to fill the viewport with the current rect.
         */
        float totalScale = currRectToDrwScale * currRectToVpScale;

        float translX = totalScale * ((int) mDrawableRect.getCenter().getPointX() - (int) currentRect.left);
        float translY = totalScale * ((int) mDrawableRect.getCenter().getPointY() - (int) currentRect.top);

        /**
         * Performs matrix transformations to fit the content
         * of the current rect into the entire view.
         */
        mMatrix.reset();
        mMatrix.postTranslate(-mDrawableRect.getWidth() / 2, -mDrawableRect.getHeight() / 2);
        mMatrix.postScale(totalScale, totalScale);
        mMatrix.postTranslate(translX, translY);
        canvas.setMatrix(mMatrix);
        canvas.drawPixelMapHolderRect(new PixelMapHolder(mPixelMap), mDrawableRect, new Paint());

        /**
         * Current transition is over. It's time to start a new one.
         */
        if (mElapsedTime >= mCurrentTrans.getDuration()) {
            fireTransitionEnd(mCurrentTrans);
            startNewTransition();
        }
    }

    /**
     * A transition listener receives notifications when a transition starts or ends.
     */
    public interface TransitionListener {
        /**
         * Notifies the start of a transition.
         *
         * @param transition the transition that just started.
         */
        void onTransitionStart(Transition transition);

        /**
         * Notifies the end of a transition.
         *
         * @param transition the transition that just ended.
         */
        void onTransitionEnd(Transition transition);
    }
}
